<?php
/* SVN FILE: $Id: file_upload.php 1895 2008-10-27 12:36:16Z francis $ */
/**
 * Generic File Upload Behavior
 *
 * This behavior is meant to be used and should be used by any other behavior
 * that is going to do anyting with file uploads.
 *
 * @todo adding support to handle multiple files at once later.
 * @todo replace $this->__file with $this->runtime[$Model->alias]['callBack']
 *       where the data is needed only in the next callback
 * @todo implement allow/disallow and overrideDuplicates
 */
class FileUploadBehavior extends ModelBehavior {
/**
 * Settings array
 *
 * @var array
 * @access public
 */
	var $settings = array();
/**
 * Default settings array
 *
 * - fileField (string, default file)
 *   The post field that holds the uploaded file info
 *
 * - basePath (string, default null)
 *   If empty the basePath is APP/tmp/ModelAlias/
 *   Notice that it must end with a /
 *
 * - randomPath (bool, false)
 *   If the to true it generates 3 level deep semi random path to avoid
 *   file system performance problems. Please check __randomPath() for more information
 *
 * - IdAsPath (bool, false)
 *   If set to true it adds the id as directory to the basepath
 *
 * - combinePathAndFile (bool, default false)
 *   if true path and filename are stored in one field
 *   if not two fields in the db table are used for filename and path
 *   you can define them using dirField and filenameField, they have default names
 *
 * - enableHash (bool, default false)
 *   If set to true an md5 hash of the file is generated
 *
 * - hashField (string, default hash)
 *   If a hash is generated its stored in the field
 *
 * - validate (bool, default true)
 *   Controlls if mime and file extension should be validated
 *
 * - allowedMime (array, default array(*), all)
 *   An array of allowed mime types
 *
 * - mimeField (string, default mime_type)
 *   Field to store the mime type
 *
 * - sizeField (string, default size)
 *   holds the name of the table field that stores the size value
 *
 * - beautifyFilename (bool)
 *   Removes white spaces from file name and replaces them by _
 *
 * @var array
 * @access public
 */
	var $defaults = array(
		'basePath' => null, // basePath
		'randomPath' => false, // basePath.##.##.##
		'idAsPath' => false, // basePath.id
		'storeFileAsId' => false, // filename == id
		'combinePathAndFile' => false, // dirField => path.file
		'dirField' => 'path',
		'filenameField' => 'filename',
		'fileField' => 'file',
		'sizeField' => 'filesize',
		'hashField' => 'hash',
		'mimeField' => 'mime_type',
		'enableHash' => false,
		'validate' => true,
		'allowedMime' => array('*'), //array('image/jpeg', 'image/pjpeg', 'image/gif', 'image/png'),
		//'allowedExt' => array('*'), //array('jpg','jpeg','gif','png'),
		//'allowDuplicates' => false,
		'beautifyFilename' => false,
		'checkMIME' => false
	);
/**
 * Error message
 *
 * If something fails this is populated with an errormsg that can be passed to the view
 *
 * @todo implement the use of this var in the behavior
 *
 * @var string
 * @access public
 */
	var $error = null;
/**
 * File resource
 *
 * Used to collect informations about the file to carry it trough the whole
 * process and to have them accessible after saving to handle saving of the file data.
 *
 * @var array
 * @access private
 */
	var $__file = array();
/**
 * Behavior setup
 *
 * Merge settings with default config, then it is checking if the target directory
 * exists and if it is writeable. It will throw an error if one of both fails.
 *
 * @param AppModel $Model
 * @param array $settings
 * @access public
 */
	function setup(&$Model, $settings = array()) {
		if (!isset($this->settings[$Model->alias])) {
			$this->settings[$Model->alias] = $this->defaults;
		}
		$this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], ife(is_array($settings), $settings, array()));

		if (empty($this->settings[$Model->alias]['basePath'])) {
			$this->settings[$Model->alias]['basePath'] = APP . 'tmp' . DS . strtolower($Model->alias) . DS;
		}
	}
/**
 * Before validation callback
 *
 * Check if the file is really an uploaded file
 * and run custom checks for file extensions and / or mime type if configured to do so.
 *
 * @param AppModel $Model
 * @access public
 */
	function beforeValidate(&$Model) {

		extract($this->settings[$Model->alias]);

		if ($validate === true) {
			if ($Model->data[$Model->alias][$fileField]['error'] !== 0) {
				switch ($Model->data[$Model->alias][$fileField]['error']) {
					case UPLOAD_ERR_OK:
					break;
					case UPLOAD_ERR_INI_SIZE:
						$this->error = __('The uploaded file exceeds the upload_max_filesize directive ('.ini_get('upload_max_filesize').') in php.ini.', true);
					break;
					case UPLOAD_ERR_FORM_SIZE:
						$this->error = __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', true);
					break;
					case UPLOAD_ERR_PARTIAL:
						$this->error = __('The uploaded file was only partially uploaded.', true);
					break;
					case UPLOAD_ERR_NO_FILE:
						$this->error = __('No file was uploaded.', true);
					break;
					case UPLOAD_ERR_NO_TMP_DIR:
						$this->error = __('The remote server has no temporary folder for file uploads.', true);
					break;
					case UPLOAD_ERR_CANT_WRITE:
						$this->error = __('Failed to write file to disk.', true);
					break;
					default:
						$this->error = __('Unknown File Error. Check php.ini settings.', true);
					break;
				}
				return false;
			}

			if (!empty($Model->data[$Model->alias][$fileField])) {
				if (!is_uploaded_file($Model->data[$Model->alias][$fileField]['tmp_name'])) {
					$this->error = __('The uploaded file is no valid upload.', true);
					$Model->invalidate($fileField);
					return false;
				}
			}

			if ($this->settings[$Model->alias]['checkMIME']) {
				if (!$this->mimeType($Model->data[$Model->alias][$this->settings[$Model->alias]['fileField']]['tmp_name'], $allowedMime)) {
					$this->error = __('You are not allowed to upload files of this type.', true);
					$Model->invalidate($fileField);
					return false;
				}
			}
		}
		return true;
	}
/**
 * Before save callback
 *
 * Collecting and generating meta data in here because we only want to
 * move the file after the meta data was saved. File data is stored in
 * $this->__file to make sure it is aviliable after save.
 *
 * @todo make it possible to use fileField also as fielenameField
 *
 * @param AppModel $Model
 * @access public
 */
	function beforeSave(&$Model) {

		if (!is_dir($this->settings[$Model->alias]['basePath'])) {
			if (!class_exists('folder')) {
				App::import('Core', 'Folder');
			}
			$Folder = new Folder($this->settings[$Model->alias]['basePath']);

			if(!$Folder->create()) {
				trigger_error(sprintf(__('Unable to create directory %s for file uploads.', true),
					$this->settings[$Model->alias]['basePath']), E_USER_ERROR);
			}
		}
		/**
		if (!is_writable($this->settings[$Model->alias]['basePath'])) {
			if (!class_exists('folder')) {
				App::import('Core', 'Folder');
			}
			$Folder = new Folder($this->settings[$Model->alias]['basePath']));

			if (!$Folder->chmod($this->settings[$Model->alias]['basePath'], 0777)) {
				trigger_error(sprintf(__('Unable to write files into directory %s for file uploads.', true),
				$this->settings[$Model->alias]['basePath']), E_USER_ERROR);
			}
		}
		*/
		extract($this->settings[$Model->alias]);

		if ($beautifyFilename === true) {
			$Model->data[$Model->alias][$fileField]['name'] = __beautify($Model->data[$Model->alias][$fileField]['name']);
		}

		if ($combinePathAndFile === true) {
			$this->data[$Model->alias][$dirField] = $basePath.$Model->data[$Model->alias][$fileField]['name'];
		}

		$Model->data[$Model->alias][$mimeField] = $this->mimeType($Model->data[$Model->alias][$fileField]['tmp_name']);
		$Model->data[$Model->alias][$filenameField] = $Model->data[$Model->alias][$fileField]['name'];
		$Model->data[$Model->alias][$sizeField] = $Model->data[$Model->alias][$fileField]['size'];
		$Model->data[$Model->alias][$dirField] = $basePath;

		if ($enableHash === true) {
			$this->data[$Model->alias][$hashField] = md5_file($Model->data[$Model->alias][$fileField]['tmp_name']);
		}

		$this->__file['filenameField'] = $Model->data[$Model->alias][$fileField]['name'];
		$this->__file['tmp_name'] = $Model->data[$Model->alias][$fileField]['tmp_name'];
		$this->__file['name'] = $Model->data[$Model->alias][$fileField]['name'];
		unset($Model->data[$Model->alias][$fileField]);
	}
/**
 * after save callback
 *
 * @param AppModel $Model
 * @access public
 */
	function afterSave(&$Model) {
		$this->__saveFile($Model);
	}
/**
 * before delete callback
 *
 * Note that it is not required to set the id, because it is already set
 * trough the Model::del() method.
 *
 * @todo work here
 *
 * @access public
 */
	function beforeDelete(&$Model) {
		$Model->recursive = -1;
		$row = $Model->read();
		$this->__file = $row[$Model->alias];
	}
/**
 * after delete callback
 *
 * @todo work here
 *
 * @param AppModel $Model
 * @access public
 */
	function afterDelete(&$Model) {
		extract($this->settings[$Model->alias]);

		if (!class_exists('file')) {
			App::import('Core', 'File');
		}

		if ($combinePathAndFile === true) {
			$path = $this->__file[$dirField];
		} else {
			$path = WWW_ROOT . 'files' . DS . $this->__file[$filenameField];
		}

		$File = new File($path);

		if (!$File->delete()) {
			trigger_error(sprintf(__('Unable to remove file %s.', true), $path), E_USER_ERROR);
		}
	}
/**
 * Check mime type type, works with path and uploaded file
 *
 * Please note that, if the standard method for php4/5 is used
 * http://www.php.net/manual/en/function.mime-content-type.php
 * the results _could be wrong_! Check the comments there.
 *
 * You can NEVER be sure that the mime type is 100% correct!
 * Use it with care, don't depend on it!
 *
 * @param string $file File to check
 * @param array $mimeTypes mime types to follow
 * @return mixed returns bool if 2nd param is used
 * @access public
 */
	function mimeType($file, $mimeTypes = array()) {
		if (!is_file($file)) {
			return false;
		}

		if (function_exists('finfo_open')) {
			$finfo = finfo_open(FILEINFO_MIME, 'C:\AppServ\php5\extras\magic');
			$mimeType = finfo_file($finfo, $file);
			finfo_close($finfo);
		} else {
			$mimeType = mime_content_type($file);
		}

		if (!empty($mimeTypes)) {
			if (in_array($mimeType, $mimeTypes) || in_array('*', $mimeTypes)) {
				return true;
			}
		} else {
			return $mimeType;
		}

		return false;
	}
/**
 * processes a file after it was uploaded
 *
 * If the files can't saved for some reason remove the previous saved meta data
 * from the database table.
 *
 * @param AppModel $Model
 * @access private
 */
	function __saveFile(&$Model) {
		extract($this->settings[$Model->alias]);

		$path = $basePath;
		if ($randomPath === true) {
			$path = $this->settings[$Model->alias]['basePath'] . $this->__randomPath($Model->id);
		}
		if ($idAsPath === true) {
			$path = $this->settings[$Model->alias]['basePath'] . $Model->id . DS;
		}
		if ($storeFileAsId === true) {
			$Model->data[$Model->alias][$filenameField] = $Model->id;
			$this->__file['filenameField'] = $Model->id;
		}
		if ($combinePathAndFile === true) {
			$Model->data[$Model->alias][$dirField] = $path.$this->__file['filenameField'];
		}
		$this->__file['path'] = $path;

		if (!$this->__beforeSaveFile($Model)) {
			return false;
		}

		if (!is_dir($this->__file['path'])) {
			if (!class_exists('folder')) {
				App::import('Core', 'Folder');
			}
			$Folder = new Folder($this->__file['path']);

			if(!$Folder->create()) {
				$Model->del($Model->id);
				trigger_error(sprintf(__('Unable to create directory %s for file uploads.', true),
					$this->__file['path']), E_USER_ERROR);
				return false;
			}
		}

		if (!move_uploaded_file($this->__file['tmp_name'], $this->__file['path'] . $this->__file['filenameField'])) {
			$Model->del($Model->id);
			trigger_error(sprintf(__('Unable to move uploaded file %s to destination %s.', true),
				$this->__file['tmp_name'], $basePath), E_USER_ERROR
			);
			return false;
		}

		/*
		 * Saving stuff to the model that could be built only after the entry was already saved.
		 */
		$Model->Behaviors->disable('FileUpload');
		$Model->save();
		$Model->Behaviors->enable('FileUpload');

		if (!$this->__afterSaveFile($Model)) {
			return false;
		}

		return true;
	}
/*
 * before save file callback for behaviors extending this class
 *
 * @param AppModel $Model
 * @access private
 */
	function __beforeSaveFile(&$Model) {
		return true;
	}
/*
 * after save file callback for behaviors extending this class
 *
 * @param AppModel $Model
 * @access private
 */
	function __afterSaveFile(&$Model) {
		return true;
	}
/*
 * Builds a semi random path based on the id to avoid having thousands of files
 * or directories in one directory. This would result in a slowdown on most file systems.
 *
 * Works up to 5 level deep
 *
 * @param mixed $id
 * @param integer $level
 * @return mixed
 */
	function __randomPath($id, $level = 3) {
		if (!$id) {
			return false;
		}
		$id = crc32($id);

		$decrement = 0;
		$path = null;
		for ($i = 0; $i < $level; $i++) {
			$decrement = $decrement -2;
			$path .=  sprintf("%02d".DS,substr('000000' . $id, $decrement, 2));
		}
		return $path;
	}
/*
 * Makes a filename looking nice
 *
 * Replaces spaces by default with underscore
 *
 * @param string $string
 * @param array $replacements
 */
	function __beautify($string, $replacements = array(' ', '_')) {
		$string = preg_replace(array('/[^\w\s]/', '/\\s+/') , $replacements, $string);
		return $string;
	}
}
?>